﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading;
using System.Windows.Media.Imaging;

namespace FileArchive
{
    public partial class ThumbnailView : UserControl
    {
        Dictionary<int, Image> thumbCache = new Dictionary<int, Image>();
        LinkedList<int> cacheList = new LinkedList<int>();
        private const int CacheSize = 500;
        void InsertThumbCache(int id, Image img)
        {
            thumbCache[id] = img;
            cacheList.AddLast(id);
            while (cacheList.Count > CacheSize)
            {
                Image timg;
                // find a cache item to kick off
                // prefer invisible items
                int clientHeight = ClientRectangle.Height;
                int kickedId = cacheList.First.Value;
                var node = cacheList.First;
                while (node != null)
                {
                    var rect = GetItemRectangle(node.Value);
                    if (rect.Bottom < 0 || rect.Top > clientHeight)
                    {
                        kickedId = node.Value;
                        break;
                    }
                    node = node.Next;
                }
                if (node == null)
                    cacheList.RemoveFirst();
                else
                    cacheList.Remove(node);
                if (thumbCache.TryGetValue(kickedId, out timg))
                {
                    timg.Dispose();
                    thumbCache.Remove(kickedId);
                }
            }
        }
        Image TryGetThumb(int id)
        {
            Image img = null;
            if (thumbCache.TryGetValue(id, out img))
                return img;
            else
                return null;
        }
        void ClearCache()
        {
            foreach (var val in thumbCache.Values)
                val.Dispose();
            thumbCache.Clear();
            cacheList.Clear();
        }
        CountdownEvent ioThreadTerminatedEvent = new CountdownEvent(0);
        bool ioThreadCancellation = false;
        private const int ioThreads = 3;
        void StartIOThread()
        {
            ioThreadTerminatedEvent.Wait();
            ioThreadTerminatedEvent.Reset(ioThreads);
            ioThreadCancellation = false;
            for (int t = 0; t < ioThreads; t++)
            {
                int threadId = t;
                new Thread(() =>
                {
                    try
                    {
                        while (!ioThreadCancellation)
                        {
                            Thread.Sleep(50);
                            int cols = Columns;
                            int startId = ((scrollBar.Value - topMargin) / RowSize) * cols;

                            for (int i = startId; i < curDir.Children.Count; i++)
                            {
                                if ((i>>3) % ioThreads != threadId)
                                    continue;
                                if (ioThreadCancellation)
                                    break;
                                if (!(curDir.Children[i] is FileNode))
                                    continue;
                                if (ImageExtensions.Contains(System.IO.Path.GetExtension(curDir.Children[i].Name).ToLower()))
                                {
                                    var rect = GetItemRectangle(i);
                                    if (rect.Y > ClientRectangle.Height && cacheList.Count >= CacheSize)
                                        break;
                                    if (!thumbCache.ContainsKey(i))
                                    {
                                        var thumb = LoadThumb(curDir.Children[i] as FileNode);
                                        lock (thumbCache)
                                        {
                                            InsertThumbCache(i, thumb);
                                        }
                                        try
                                        {
                                            BeginInvoke(new Action(() => { InvalidateItem(i); }));
                                        }
                                        catch (Exception)
                                        {

                                        }
                                        break;
                                    }
                                }
                            }


                        }
                    }
                    finally
                    {
                        ioThreadTerminatedEvent.Signal();
                    }
                }).Start();
            }
        }

        private void StopThread()
        {
            ioThreadCancellation = true;
            ioThreadTerminatedEvent.Wait();
        }

        Size ScaleTo(int width, int height, int targetSize)
        {
            Size rs = new Size();
            if (width > height)
            {
                rs.Width = targetSize;
                rs.Height = height * targetSize / width;
            }
            else
            {
                rs.Height = targetSize;
                rs.Width = width * targetSize / height;
            }
            return rs;
        }

        public event EventHandler PathChanged;
        private void OnPathChanged()
        {
            if (PathChanged != null)
                PathChanged(this, EventArgs.Empty);
        }

        private Image LoadThumb(FileNode fileNode)
        {
            try
            {
                using (var ms = archive.LoadFile(fileNode))
                using (var img = Image.FromStream(ms, true, false))
                {
                    foreach (var pi in img.PropertyItems)
                        if (pi.Id == 20507)
                            return (Bitmap)Image.FromStream(new MemoryStream(pi.Value));
                    var size = ScaleTo(img.Width, img.Height, 256);
                    return img.GetThumbnailImage(size.Width, size.Height, null, IntPtr.Zero);
                }
            }
            catch (Exception)
            {
                return generalDocumentImage;
            }
        }
        private Image generalDocumentImage = ExtractVistaIcon(Resource.Generic_Document);
        private Image imageFileIcon = ExtractVistaIcon(Resource.generic_picture);
        Bitmap folderIcon = null;
        private Archive archive;
        internal Archive Archive
        {
            get 
            { 
                return archive; 
            }
            set
            {
                archive = value;
                CurrentDirectory = archive.Root;
            }
        }
        private FolderNode curDir;
        internal FolderNode CurrentDirectory
        {
            get
            {
                return curDir;
            }
            set
            {
                DirectoryChanged(value);
            }
        }
        private const int topMargin = 5;
        private const int leftMargin = 10;
        private const int bottomMargin = 5;
        private const int rightMargin = 10;
        private int thumbSize = 128;
        public int ThumbSize
        {
            get { return thumbSize; }
            set
            {
                thumbSize = value;
                if (folderIcon != null)
                    folderIcon.Dispose();
                if (thumbSize > 48)
                {
                    folderIcon = ExtractVistaIcon(Resource.Folder_stuffed);
                }
                else
                    folderIcon = new Icon(Resource.Folder_stuffed, thumbSize, thumbSize).ToBitmap();
                if (curDir != null)
                    ResetScrollbar();
            }
        }
        static Bitmap ExtractVistaIcon(Icon icoIcon)
        {
            Bitmap bmpPngExtracted = null;
            try
            {
                byte[] srcBuf = null;
                using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
                { icoIcon.Save(stream); srcBuf = stream.ToArray(); }
                const int SizeICONDIR = 6;
                const int SizeICONDIRENTRY = 16;
                int iCount = BitConverter.ToInt16(srcBuf, 4);
                for (int iIndex = 0; iIndex < iCount; iIndex++)
                {
                    int iWidth = srcBuf[SizeICONDIR + SizeICONDIRENTRY * iIndex];
                    int iHeight = srcBuf[SizeICONDIR + SizeICONDIRENTRY * iIndex + 1];
                    int iBitCount = BitConverter.ToInt16(srcBuf, SizeICONDIR + SizeICONDIRENTRY * iIndex + 6);
                    if (iWidth == 0 && iHeight == 0 && iBitCount == 32)
                    {
                        int iImageSize = BitConverter.ToInt32(srcBuf, SizeICONDIR + SizeICONDIRENTRY * iIndex + 8);
                        int iImageOffset = BitConverter.ToInt32(srcBuf, SizeICONDIR + SizeICONDIRENTRY * iIndex + 12);
                        System.IO.MemoryStream destStream = new System.IO.MemoryStream();
                        System.IO.BinaryWriter writer = new System.IO.BinaryWriter(destStream);
                        writer.Write(srcBuf, iImageOffset, iImageSize);
                        destStream.Seek(0, System.IO.SeekOrigin.Begin);
                        bmpPngExtracted = new Bitmap(destStream); // This is PNG! :)
                        break;
                    }
                }
            }
            catch { return null; }
            return bmpPngExtracted;
        }
        internal List<FolderNode> Path { get; private set; }
        private const int BorderSize = 10;
        private const int LineHeight = 33;
        private int Columns { get { return Math.Max(1, (ClientSize.Width - BorderSize-leftMargin-rightMargin) / (ThumbSize + BorderSize)); } }
        private int RowSize { get { return ThumbSize + BorderSize + LineHeight; } }
        private int ColSize { get { return BorderSize + ThumbSize; } }
        private void ResetScrollbar()
        {
            int cols = Columns;
            if (cols == 0) cols = 1;
            int rows = (curDir.Children.Count + cols - 1) / cols;
            int max = rows * RowSize + topMargin + bottomMargin;
            if (max > ClientSize.Height)
            {
                if (!scrollBar.Visible)
                {
                    scrollBar.Visible = true;
                    //ResetScrollbar();
                }
                if (scrollBar.Value > max - ClientSize.Height)
                    scrollBar.Value = Math.Max(0, max - ClientSize.Height);
                scrollBar.Maximum = max;
                scrollBar.LargeChange = ClientSize.Height;
                scrollBar.SmallChange = 30;
            }
            else
            {
                scrollBar.Value = 0;
                scrollBar.Visible = false;
            }
        }
        private void InvalidateItem(int id)
        {
            Invalidate(GetItemRectangle(id));
        }
        private void DirectoryChanged(FolderNode newDir)
        {
            StopThread();
            Path.Clear();
            var cd = newDir;
            while (cd != null)
            {
                Path.Add(cd);
                cd = cd.Parent;
            }
            curDir = newDir;
            ResetScrollbar();
            scrollBar.Value = 0;
            selectedItemIndex = -1;
            highlightedItemIndex = -1;
            ClearCache();
            StartIOThread();
            Invalidate();
            OnPathChanged();
        }
        public ThumbnailView()
        {
            InitializeComponent();
            Path = new List<FolderNode>();
            ThumbSize = 128;
            SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true);
            this.MouseWheel += ThumbnailView_MouseWheel;
        }

        void ThumbnailView_MouseWheel(object sender, MouseEventArgs e)
        {
            if (scrollBar.Visible)
                scrollBar.Value = Math.Max(0, Math.Min(scrollBar.Maximum-scrollBar.LargeChange, scrollBar.Value - e.Delta));
        }

        private void ThumbnailView_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.FillRectangle(SystemBrushes.Window, e.ClipRectangle);
            if (curDir != null)
            {
                DrawItems(e.Graphics, e.ClipRectangle);
            }
        }

        private int highlightedItemIndex = -1;
        private int selectedItemIndex = -1;
        private static Color highlightColor1 = Color.FromArgb(250,251,253);
        private static Color highlightColor2 = Color.FromArgb(235, 243, 253);
        private static Color highlightBorderColor = Color.FromArgb(184, 214, 251);
        private static Color selectionColor1 = Color.FromArgb(220, 234, 252);
        private static Color selectionColor2 = Color.FromArgb(193, 220, 252);
        private static Color selectionBorderColor = Color.FromArgb(125, 162, 206);
       
        private void DrawItems(Graphics g, Rectangle clipRect)
        {
            StringFormat stringFormat = new StringFormat();
            stringFormat.Alignment = StringAlignment.Center;
            stringFormat.LineAlignment = StringAlignment.Near;
            stringFormat.Trimming = StringTrimming.EllipsisWord;
            using (var highlightBorderPen = new Pen(highlightBorderColor))
            using (var selectionBorderPen = new Pen(highlightBorderColor))
            {
                int cols = Columns;
                int startId = ((scrollBar.Value + clipRect.Top - topMargin) / RowSize) * cols;
                for (int i = startId; i < curDir.Children.Count; i++)
                {
                    Rectangle blockRect = GetItemRectangle(i);
                    blockRect.Width--;
                    blockRect.Height--;
                    if (blockRect.Y > clipRect.Bottom)
                        break;
                    if (blockRect.Right < clipRect.Left)
                        continue;
                    if (blockRect.Left > clipRect.Right)
                        continue;
                    if (i == highlightedItemIndex && i != selectedItemIndex)
                    {
                        using (Brush b = new LinearGradientBrush(blockRect, highlightColor1, highlightColor2, LinearGradientMode.Vertical))
                            g.FillRectangle(b, blockRect);
                        g.DrawRectangle(highlightBorderPen, blockRect);
                    }
                    if (i == selectedItemIndex)
                    {
                        using (Brush b = new LinearGradientBrush(blockRect, selectionColor1, selectionColor2, LinearGradientMode.Vertical))
                            g.FillRectangle(b, blockRect);
                        g.DrawRectangle(selectionBorderPen, blockRect);
                    }
                    // Draw Thumb
                    Rectangle thumbRect = new Rectangle(blockRect.X + BorderSize / 2, blockRect.Y + BorderSize / 2, ThumbSize, ThumbSize);
                   
                    if (curDir.Children[i] is FolderNode)
                    {
                        g.DrawImage(folderIcon, thumbRect);
                    }
                    else if (ImageExtensions.Contains(System.IO.Path.GetExtension(curDir.Children[i].Name).ToLower()))
                    {
                        Rectangle contentRect;
                        var img = TryGetThumb(i);
                        if (img != null)
                        {

                            try
                            {
                                var size = ScaleTo(img.Width, img.Height, thumbSize);
                                thumbRect.Width = size.Width;
                                thumbRect.Height = size.Height;
                                thumbRect.Y += thumbSize - size.Height;
                                thumbRect.X += (thumbSize - size.Width) / 2;
                                contentRect = DropShadow.GetContentRect(thumbRect);
                                g.DrawImage(img, contentRect);
                                DropShadow.DrawRectangle(g, thumbRect);
                            }
                            catch (Exception)
                            {
                                g.DrawImage(imageFileIcon, thumbRect);
                            }
                        }
                        else
                        {
                            contentRect = DropShadow.GetContentRect(thumbRect);
                            g.DrawImage(imageFileIcon, contentRect);
                            //g.FillRectangle(SystemBrushes.Window, contentRect);
                        }                        
                    }
                    else
                    {
                        g.DrawImage(generalDocumentImage, thumbRect);
                    }
                    // Draw Text
                    Rectangle textRect = new Rectangle(thumbRect.Left, thumbRect.Bottom + 2, thumbRect.Width, LineHeight - 2);
                    g.DrawString(curDir.Children[i].Name, Font, SystemBrushes.WindowText, textRect, stringFormat);
                }
            }
            stringFormat.Dispose();
        }

        public new void Dispose()
        {
            base.Dispose();
            StopThread();
        }

        private Rectangle GetItemRectangle(int id)
        {
            int cols = Columns;
            int col = id % cols;
            int row = id / cols;
            int x0 = col * (BorderSize + ThumbSize) + leftMargin;
            int y0 = row * RowSize - scrollBar.Value + topMargin;
            return new Rectangle(x0, y0, ColSize, RowSize);
        }

        private int HitTest(int x, int y)
        {
            int cols = Columns;
            int col = (x-leftMargin) / ColSize;
            if (col >= cols)
                return -1;
            int row = (y + scrollBar.Value - topMargin) / RowSize;
            int id = row * cols + col;
            if (id < curDir.Children.Count)
                return id;
            else
                return -1;
        }

        private void scrollBar_ValueChanged(object sender, EventArgs e)
        {
            Invalidate();
        }

        private void ThumbnailView_Resize(object sender, EventArgs e)
        {
            if (curDir != null)
            {
                ResetScrollbar();
                Invalidate();
            }
        }

        private void ThumbnailView_MouseMove(object sender, MouseEventArgs e)
        {
            int newHighlight = HitTest(e.X, e.Y);
            if (newHighlight != highlightedItemIndex)
            {
                int oldHighlight = highlightedItemIndex;
                highlightedItemIndex = newHighlight;
                InvalidateItem(oldHighlight);
                InvalidateItem(highlightedItemIndex);
            }
        }

        private void ThumbnailView_MouseLeave(object sender, EventArgs e)
        {
            if (highlightedItemIndex != -1)
            {
                int oldHighlight = highlightedItemIndex;
                highlightedItemIndex = -1;
                InvalidateItem(oldHighlight);
            }
        }

        private void ThumbnailView_MouseDown(object sender, MouseEventArgs e)
        {
            int newSelection = HitTest(e.X, e.Y);
            if (newSelection != selectedItemIndex)
            {
                int oldSelection = selectedItemIndex;
                selectedItemIndex = newSelection;
                InvalidateItem(oldSelection);
                InvalidateItem(selectedItemIndex);
            }
        }

        private void ThumbnailView_DoubleClick(object sender, EventArgs e)
        {
            if (selectedItemIndex != -1)
            {
                if (curDir.Children[selectedItemIndex] is FolderNode)
                {
                    CurrentDirectory = curDir.Children[selectedItemIndex] as FolderNode;
                }
                else
                {
                    OnFileItemDoubleClick();
                }
            }
        }
        public event EventHandler FileItemDoubleClick;
        public int SelectedIndex
        {
            get
            {
                return selectedItemIndex;
            }
        }
        private void OnFileItemDoubleClick()
        {
            if (FileItemDoubleClick != null)
                FileItemDoubleClick(this, EventArgs.Empty);
        }
    }
}
